package inode

import (
	reqContext "context"
	"regexp"
	"time"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/util/comm"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/util/endpoint"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/protoutil"
	"github.com/pkg/errors"
	x509GM "github.com/tjfoc/gmsm/x509"
	"google.golang.org/grpc"
	"google.golang.org/grpc/keepalive"
	grpcstatus "google.golang.org/grpc/status"

	"git.cloud.inspur.com/ichain/ichain-protos-go/common"
	pb "git.cloud.inspur.com/ichain/ichain-protos-go/inode"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/status"
)

const (
	maxCallRecvMsgSize = 100 * 1024 * 1024
	maxCallSendMsgSize = 100 * 1024 * 1024
)

var contractNotFoundPattern = regexp.MustCompile(`(contract [^ ]+ not found)|(could not find contract with name)|(cannot get package for contract)`)

// inodeEndorser enables access to a GRPC-based endorser for running transaction proposal simulations
type inodeEndorser struct {
	grpcDialOption []grpc.DialOption
	target         string
	dialTimeout    time.Duration
	commManager    ichain.CommManager
}

type inodeEndorserRequest struct {
	target             string
	certificate        *x509GM.Certificate
	serverHostOverride string
	config             ichain.EndpointConfig
	kap                keepalive.ClientParameters
	failFast           bool
	allowInsecure      bool
	commManager        ichain.CommManager
}

func newInodeEndorser(endorseReq *inodeEndorserRequest) (*inodeEndorser, error) {
	if len(endorseReq.target) == 0 {
		return nil, errors.New("target is required")
	}

	// Construct dialer options for the connection
	var grpcOpts []grpc.DialOption
	if endorseReq.kap.Time > 0 {
		grpcOpts = append(grpcOpts, grpc.WithKeepaliveParams(endorseReq.kap))
	}
	grpcOpts = append(grpcOpts, grpc.WithDefaultCallOptions(grpc.WaitForReady(!endorseReq.failFast)))

	if endpoint.AttemptSecured(endorseReq.target, endorseReq.allowInsecure) {
		credential, err := comm.ParseCertToTransportCredential(endorseReq.certificate,
			endorseReq.serverHostOverride, endorseReq.config)
		if err != nil {
			return nil, err
		}
		grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(credential))
	} else {
		grpcOpts = append(grpcOpts, grpc.WithInsecure())
	}

	grpcOpts = append(grpcOpts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxCallRecvMsgSize),
		grpc.MaxCallSendMsgSize(maxCallSendMsgSize)))

	timeout := endorseReq.config.Timeout(ichain.InodeConnection)

	pc := &inodeEndorser{
		grpcDialOption: grpcOpts,
		target:         endpoint.ToAddress(endorseReq.target),
		dialTimeout:    timeout,
		commManager:    endorseReq.commManager,
	}

	return pc, nil
}

// ProcessTransactionProposal sends the transaction proposal to a inode and returns the response.
func (p *inodeEndorser) ProcessTransactionProposal(ctx reqContext.Context, request ichain.ProcessProposalRequest) (*ichain.TransactionProposalResponse, error) {
	logger.Debugf("Processing proposal using endorser: %s", p.target)

	proposalResponse, err := p.sendProposal(ctx, request)
	if err != nil {
		tpr := ichain.TransactionProposalResponse{Endorser: p.target}
		return &tpr, errors.Wrapf(err, "Transaction processing for endorser [%s]", p.target)
	}

	contractStatus, err := getContractResponseStatus(proposalResponse)
	if err != nil {
		return nil, errors.WithMessage(err, "contract response status parsing failed")
	}

	tpr := ichain.TransactionProposalResponse{
		ProposalResponse: proposalResponse,
		Endorser:         p.target,
		ContractStatus:   contractStatus,
		Status:           proposalResponse.GetResponse().Status,
	}
	return &tpr, nil
}

func (p *inodeEndorser) conn(ctx reqContext.Context) (*grpc.ClientConn, error) {
	commManager, ok := context.RequestCommManager(ctx)
	if !ok {
		commManager = p.commManager
	}

	ctx, cancel := reqContext.WithTimeout(ctx, p.dialTimeout)
	defer cancel()

	return commManager.DialContext(ctx, p.target, p.grpcDialOption...)
}

func (p *inodeEndorser) releaseConn(ctx reqContext.Context, conn *grpc.ClientConn) {
	commManager, ok := context.RequestCommManager(ctx)
	if !ok {
		commManager = p.commManager
	}

	commManager.ReleaseConn(conn)
}

func (p *inodeEndorser) sendProposal(ctx reqContext.Context, proposal ichain.ProcessProposalRequest) (*pb.ProposalResponse, error) {
	conn, err := p.conn(ctx)
	if err != nil {
		rpcStatus, ok := grpcstatus.FromError(err)
		if ok {
			return nil, errors.WithMessage(status.NewFromGRPCStatus(rpcStatus), "connection failed")
		}
		return nil, status.New(status.EndorserClientStatus, status.ConnectionFailed.ToInt32(), err.Error(), []interface{}{p.target})
	}
	defer p.releaseConn(ctx, conn)

	endorserClient := pb.NewEndorserClient(conn)
	resp, err := endorserClient.ProcessProposal(ctx, proposal.SignedProposal)

	if err != nil {
		logger.Errorf("process proposal failed [%s]", err)
		rpcStatus, ok := grpcstatus.FromError(err)

		if ok {
			err = status.NewFromGRPCStatus(rpcStatus)
		}
	} else {
		err = extractContractErrorFromResponse(resp)
	}

	return resp, err
}

// extractContractErrorFromResponse extracts contract error from proposal response
func extractContractErrorFromResponse(resp *pb.ProposalResponse) error {
	if resp.Response.Status < int32(common.Status_SUCCESS) || resp.Response.Status >= int32(common.Status_BAD_REQUEST) {
		details := []interface{}{resp.Endorsement, resp.Response.Payload}
		if contractNotFoundPattern.MatchString(resp.Response.Message) {
			return status.New(status.EndorserClientStatus, int32(status.ContractNameNotFound), resp.Response.Message, details)
		}
		return status.New(status.ContractStatus, resp.Response.Status, resp.Response.Message, details)
	}
	return nil
}

func getContractResponseStatus(response *pb.ProposalResponse) (int32, error) {
	if response.Payload != nil {
		payload, err := protoutil.UnmarshalProposalResponsePayload(response.Payload)
		if err != nil {
			return 0, errors.Wrap(err, "unmarshal of proposal response payload failed")
		}

		extension, err := protoutil.UnmarshalContractAction(payload.Extension)
		if err != nil {
			return 0, errors.Wrap(err, "unmarshal of contract action failed")
		}

		if extension != nil && extension.Response != nil {
			return extension.Response.Status, nil
		}
	}
	return response.Response.Status, nil
}
